package ua.com.stormlabs.jsitemapper;

import org.apache.log4j.Logger;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.io.FileUtils;

import java.net.URL;
import java.net.MalformedURLException;
import java.util.*;
import java.io.File;
import java.io.IOException;
import java.io.FileWriter;
import java.io.BufferedWriter;

/**
 * User: storm
 * Date: 26.03.2009
 */
public class LinkPool {

    private final static Logger log = Logger.getLogger(LinkPool.class);

    private final Map<String, URL> awaiting = new HashMap<String, URL>();
    private final Map<String, URL> processing = new HashMap<String, URL>();
    private final Set<String> failed = new HashSet<String>();
    private final Set<String> mapped = new HashSet<String>();

    private final Set<String> skipped = new HashSet<String>();

    private static LinkPool instance = new LinkPool();
    private static final String MAPPED_FILENAME = "mapped.txt";
    private static final String FAILED_FILENAME = "failed.txt";
    private static final String SKIPPED_FILENAME = "skipped.txt";
    private BufferedWriter failedFileWriter;
    private BufferedWriter mappedFileWriter;
    private static final String DELIMITER = "\n";
    private BufferedWriter skippedFileWriter;

    public static LinkPool getInstance() {
        if (instance.failedFileWriter == null || instance.mappedFileWriter == null) return null;
        return instance;
    }

    private LinkPool() {
        try {
            failedFileWriter = new BufferedWriter(new FileWriter(FAILED_FILENAME));
            mappedFileWriter = new BufferedWriter(new FileWriter(MAPPED_FILENAME));
            skippedFileWriter = new BufferedWriter(new FileWriter(SKIPPED_FILENAME));
        } catch (IOException e) {
            log.error("Failed to create file writer. Error: " + e, e);
        }
    }

    public synchronized boolean addAwaiting(String link) {
        URL url;
        try {
            url = new URL(link);
        } catch (MalformedURLException e) {
            log.error("Failed to create URL from link " + link + ". Error: " + e, e);
            return false;
        }
        link = LinkPreprocessor.getInstance().preprocess(link, url.toExternalForm());
        if (isKnown(link)) {
            log.debug("Skipping known link '" + link + "'");
            return false;
        }
        log.debug("Adding new awaiting link '" + link + "'. Total awaiting: " + awaiting.size());
        awaiting.put(link, url);
        return true;
    }

    private boolean isKnown(String link) {
        if (StringUtils.isBlank(link)) return true;
        return (awaiting.containsKey(link) || processing.containsKey(link) ||
                failed.contains(link) || mapped.contains(link));
    }

    public synchronized boolean isEmpty() {
        return (awaiting.isEmpty() && processing.isEmpty());
    }

    public synchronized URL getAwaiting() {
        if (!awaiting.isEmpty()) {
            String link = (String) awaiting.keySet().toArray()[0];
            URL url = awaiting.remove(link);
            processing.put(link, url);
            log.debug("Moving to processing link " + link + ". Total processing: " + processing.size());
            return url;
        }
        return null;
    }

    public synchronized void addMapped(URL url) {
        String link = removeByValue(processing, (url));
        if (link == null) {
            String msg = "Mapped URL " + url + " not found in processing list!";
            log.error(msg);
            throw new NoSuchElementException(msg);
        }
        mapped.add(link);
        log.debug("Adding mapped link " + link + ". Total mapped: " + mapped.size());
        try {
            mappedFileWriter.write(link + DELIMITER);
            mappedFileWriter.flush();
        } catch (IOException e) {
            log.error("Failed to write to mappedFile link '" + link + "'. Error: " + e, e);
        }
    }

    public synchronized void addFailed(URL url) {
        String link = removeByValue(processing, (url));
        if (link == null) {
            String msg = "Failed URL " + url + " not found in processing list!";
            log.error(msg);
            throw new NoSuchElementException(msg);
        }
        failed.add(link);
        log.debug("Adding failed link '" + link + "'. Total failed: " + failed.size());
        try {
            failedFileWriter.write(link + DELIMITER);
            failedFileWriter.flush();
        } catch (IOException e) {
            log.error("Failed to write to failedFile link '" + link + "'. Error: " + e, e);
        }
    }

    public synchronized void addSkipped(String link) {
        try {
            if (skipped.add(link)) {
                log.debug("Total skipped: " + skipped.size());
                skippedFileWriter.write(link + DELIMITER);
                skippedFileWriter.flush();
            }
        } catch (IOException e) {
            log.error("Failed to write to skippedFile link '" + link + "'. Error: " + e, e);
        }
    }

    private static <K, V> K removeByValue(Map<K, V> map, V value) {
        for (Iterator<Map.Entry<K, V>> it = map.entrySet().iterator(); it.hasNext();) {
            Map.Entry<K, V> entry = it.next();
            if (value.equals(entry.getValue())) {
                K key = entry.getKey();
                it.remove();
                return key;
            }
        }
        return null;
    }

    public void close() {
        try {
            mappedFileWriter.close();
        } catch (IOException e) {
            log.error("Failed to close mapped file. Error: " + e, e);
        }
        try {
            failedFileWriter.close();
        } catch (IOException e) {
            log.error("Failed to close failed file. Error: " + e, e);
        }
        try {
            skippedFileWriter.close();
        } catch (IOException e) {
            log.error("Failed to close skipped file. Error: " + e, e);
        }
    }
}
